package alarm_monitor

import (
	"context"
	"device-admin/config/redis"
	"device-admin/internal/app/grpc/service/common"
	"device-admin/internal/dal"
	"device-admin/internal/dao"
	"device-admin/internal/define"
	"encoding/json"
	"strconv"
	"strings"
	"time"

	"192.168.1.75/go-pkg/logx"
	_redis "github.com/go-redis/redis/v8"
	"github.com/golang-module/carbon/v2"
)

type alarmMsgType struct {
	AlarmType int    `json:"alarm_type"` // 告警类型
	RobotID   string `json:"robot_id"`   // 设备ID
	Battery   string `json:"battery"`    // 电量
	Content   string `json:"content"`    // 告警详情
	CreatedAt int64  `json:"created_at"` // 创建时间
}

// AlarmStatus 告警状态检查
type AlarmMonitor struct{}

type checkFunc func(robotID string, dstStatus int) bool

var alarmMergeGroupCount = make(map[int]int)

// 初始化
// 计算每个告警合并分组的成员数
func init() {
	for _, conf := range define.AlarmType {
		if conf.MergeType > 0 {
			alarmMergeGroupCount[conf.MergeType] = alarmMergeGroupCount[conf.MergeType] + 1
		}
	}
}

// getCheckTime
func getCheckTime(alarmType int, checkTime int64) int64 {
	confirmTime := define.AlarmType[alarmType].ConfirmTime
	return checkTime + int64(confirmTime) + 10
}

// 缓存告警相关的内容
func (am AlarmMonitor) cacheAlarmContent(robotID string, alarmType int, content string, battery string, expireTime int64) {
	// 缓存告警内容
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	key := strings.Join([]string{
		robotID,
		strconv.Itoa(alarmType),
	}, "_")
	rdb.HSet(
		ctx, define.RedisAlarmMonitorContent+key,
		"content", content,
		"battery", battery,
	) // 暂针对的是及时告警的，不需要保存很久
	rdb.Expire(ctx, define.RedisAlarmMonitorContent+key, time.Second*time.Duration(expireTime))
}

// PushAlarm  推送告警
func (am AlarmMonitor) PushAlarm(robotID string, alarmType int) {
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	// 获取告警内容
	key := strings.Join([]string{
		robotID,
		strconv.Itoa(alarmType),
	}, "_")
	content, _ := rdb.HGet(ctx, define.RedisAlarmMonitorContent+key, "content").Result()
	battery, _ := rdb.HGet(ctx, define.RedisAlarmMonitorContent+key, "battery").Result()

	var alarmMsg alarmMsgType
	alarmMsg.RobotID = robotID
	alarmMsg.AlarmType = alarmType
	alarmMsg.Content = content
	alarmMsg.Battery = battery
	alarmMsg.CreatedAt = time.Now().Unix()
	dataBytes, _ := json.Marshal(alarmMsg)
	rdb.LPush(ctx, define.RedisAlarmNotifyList, string(dataBytes)).Result()
}

// CheckForNotify 检查目标状态，如果不符合预期状态，则告警
func (am AlarmMonitor) CheckOrNotify(robotID string, alarmType int, checkTime int64, dstStatus int, f checkFunc) {
	// 告警过滤
	if NewFilter(robotID, alarmType, dstStatus).Filter() {
		return
	}
	// 如果预期状态不符合
	// 当状态检测函数为nil时，仅考虑状态转移和合并
	if f == nil || !f(robotID, dstStatus) {
		// 告警类型转换
		transformAlarmType := define.AlarmTypeTransform[define.AlarmTypeST(alarmType)][define.DstStatusST(dstStatus)]
		oldAlarmType := alarmType
		if transformAlarmType > 0 {
			alarmType = int(transformAlarmType)
		}
		// 是否需要告警合并
		mergeAlarmType := define.AlarmType[alarmType].MergeType
		margeTimeout := define.AlarmType[alarmType].MergeTimeout
		// 需要合并的告警
		if mergeAlarmType > 0 && !(alarmMerge{}).HasMerged(robotID, alarmType) {
			if (alarmMerge{}).delayAlarm(robotID, oldAlarmType, alarmType, checkTime, int64(margeTimeout)) {
				alarmMerge{}.mergeConfirm(robotID, mergeAlarmType)
			}
		} else { // 不需要合并或已经合并过但没有被取消的告警，直接告警
			am.PushAlarm(robotID, alarmType)
			logx.Info(
				context.Background(),
				"alarmCheck failed",
				logx.String("robot_id", robotID),
				logx.Int("alarm_type", alarmType),
				logx.Int("dstStatus", dstStatus),
			)
		}
	} else {
		logx.Info(
			context.Background(),
			"alarmCheck ok",
			logx.String("robot_id", robotID),
			logx.Int("alarm_type", alarmType),
			logx.Int("dstStatus", dstStatus),
		)
	}
}

// genAlarmSetKey 生成告警集合的key，作为监控对象
// 先获取对应设备告警key最近设置的key，暂时为2分钟，如果
func genAlarmSetKey(robotID string, alarmType int, checkTime int64) string {
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	minScore := strconv.FormatInt(checkTime-180, 10)
	maxScore := strconv.FormatInt(checkTime, 10)
	alarmSetKeys, _ := rdb.ZRevRangeByScore(ctx,
		define.RedisAlarmMonitorSet,
		&_redis.ZRangeBy{
			Min: minScore,
			Max: maxScore,
		}).Result()
	for _, alarmSetKey := range alarmSetKeys {
		if strings.HasPrefix(alarmSetKey, robotID+"_"+strconv.Itoa(alarmType)) {
			return alarmSetKey
		}
	}
	return strings.Join([]string{robotID, strconv.Itoa(alarmType), strconv.Itoa(int(checkTime))}, "_")
}

// ClearMonitor 清除计划的状态监控
func (am AlarmMonitor) ClearScheduleMonitor(robotID string) {
	rdb := redis.NewDB()
	keys, _, _ := redis.NewDB().Scan(context.Background(), 0, "*"+define.RedisAlarmMonitorStatus+robotID+"*", 1000).Result()
	for _, key := range keys {
		func() {
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
			defer cancel()
			status, err := rdb.Get(ctx, key).Int()
			if status >= 0x10 && err == nil {
				keyArr := strings.Split(key, ":")
				if len(keyArr) > 0 {
					rdb.ZRem(ctx, define.RedisAlarmMonitorSet, keyArr[len(keyArr)-1])
					rdb.Del(ctx, key)
				}
			}
		}()
	}
}

// AddMonitor 增加目标状态,期望某个机器在某个时间的状态
// 参数说明：
// checkTime-目标状态对应的时间戳
// status-计划状态时，实际存储的status为0x10+status
// force 为true时，为计划状态，可以覆盖已有的计划状态，已经是计划内的状态，不能再被更改，
func (am AlarmMonitor) AddMonitor(
	robotID string,
	alarmType int,
	checkTime int64,
	dstStatus int,
	force bool,
	content, battery string,
) {
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	// 告警过滤
	if NewFilter(robotID, alarmType, dstStatus).Filter() {
		return
	}

	// 告警限流 - 不针对计划状态
	minInterval := define.AlarmType[alarmType].MinInterval
	if minInterval > 0 && !force {
		lockKey := define.RedisAlarmMonitorRateLimit + robotID + ":" + strconv.Itoa(alarmType)
		if success, _ := rdb.SetNX(ctx, lockKey, 1, time.Second*time.Duration(minInterval)).Result(); !success {
			return
		}
	}
	// 每日最大告警次数限制
	{
		key := define.RedisAlarmMonitorMaxTimes + robotID + "_" + strconv.Itoa(alarmType)
		maxTimesOfDay := define.AlarmType[alarmType].MaxTimesOfDay
		times, _ := rdb.Get(ctx, key).Int()
		if maxTimesOfDay > 0 && !force {
			if times+1 > maxTimesOfDay {
				return
			} else {
				endUnix := carbon.CreateFromTimestamp(time.Now().Unix()).EndOfDay().Timestamp()
				rdb.Set(ctx, key, times+1, time.Second*(time.Duration(endUnix-time.Now().Unix())))
			}
		}
	}
	// 计算告警检查时间，触发时间+确认时间
	checkTime = getCheckTime(alarmType, checkTime)
	// 忽略过时的状态设置
	expireTS := checkTime - time.Now().Unix()
	if expireTS <= 0 {
		return
	}
	// 获取alarmSetKey，存在最近的key，则复用
	alarmSetKey := genAlarmSetKey(robotID, alarmType, checkTime)
	rdb.ZAdd(ctx, define.RedisAlarmMonitorSet, &_redis.Z{
		Score:  float64(checkTime), // 计算时间的分值
		Member: alarmSetKey,
	}).Result()
	// 过期时间增加30s，避免临界
	expireTS = expireTS + 30
	// 告警详情缓存
	if content != "" {
		am.cacheAlarmContent(robotID, alarmType, content, battery, expireTS+10)
	}
	// 告警目标状态缓存
	val, _ := rdb.Get(ctx, define.RedisAlarmMonitorStatus+alarmSetKey).Int()
	expireTime := time.Second * time.Duration(expireTS+30) // key过期时间
	// 计划状态
	if force {
		rdb.Set(ctx, define.RedisAlarmMonitorStatus+alarmSetKey, dstStatus|0x10, expireTime)
	} else if val < 0x10 { // 普通状态
		rdb.Set(ctx, define.RedisAlarmMonitorStatus+alarmSetKey, dstStatus, expireTime)
	} else if val >= 0x10 { // 计划状态不能被覆盖
		return
	}
}

// GetDstStatus 获取目标状态
func (am AlarmMonitor) GetDstStatus(key string) (int, error) {
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	str, err := rdb.Get(ctx, define.RedisAlarmMonitorStatus+key).Result()
	if err != nil {
		return -1, err
	}
	val, _ := strconv.Atoi(str)
	return val & 0x0f, nil
}

// DstStatusCheck 目标状态检查
func (am AlarmMonitor) DstStatusCheck() error {
	rdb := redis.NewDB()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	minScore := "0"
	maxScore := strconv.FormatInt(time.Now().Unix(), 10)
	// 获取已经到期的告警计划
	alarmSetKeys, _ := rdb.ZRevRangeByScore(ctx,
		define.RedisAlarmMonitorSet,
		&_redis.ZRangeBy{
			Min: minScore,
			Max: maxScore,
		}).Result()
	// 删除已经到期的告警计划
	rdb.ZRemRangeByScore(
		ctx,
		define.RedisAlarmMonitorSet,
		minScore,
		maxScore,
	)
	for _, alarmSetKey := range alarmSetKeys {
		alarmSchduleArr := strings.Split(alarmSetKey, "_")
		if len(alarmSchduleArr) > 2 {
			robotID := alarmSchduleArr[0]
			alarmTypeStr := alarmSchduleArr[1]
			checktime, _ := strconv.Atoi(alarmSchduleArr[2])
			alarmType, _ := strconv.Atoi(alarmTypeStr)
			dstStatus, err := am.GetDstStatus(alarmSetKey)
			if err != nil {
				logx.Error(context.Background(), "dstStatus get failed")
				continue
			}
			switch alarmType {
			case 0x0A: // 低电量告警
				am.CheckOrNotify(robotID, alarmType, int64(checktime), dstStatus, lowBatteryCheck)
			case 2001: // 95离线告警
				fallthrough
			case 2004: // 95开机失败
				fallthrough
			case 2007: // 95关机失败
				am.CheckOrNotify(robotID, alarmType, int64(checktime), dstStatus, onlineCheck95)
			case 2002: // 97离线告警
				fallthrough
			case 2005: // 97开机失败
				fallthrough
			case 2008: // 97关机失败
				am.CheckOrNotify(robotID, alarmType, int64(checktime), dstStatus, onlineCheck97)
			case 2003: // 98离线告警
				fallthrough
			case 2006: // 98开机失败
				fallthrough
			case 2009: // 98关机失败
				am.CheckOrNotify(robotID, alarmType, int64(checktime), dstStatus, onlineCheck98)
			// 默认不需要确认的告警,仅需要考虑告警合并
			default:
				am.CheckOrNotify(robotID, alarmType, int64(checktime), dstStatus, nil)
			}
		}
	}
	return nil
}

// 状态检查规则实现
// 0 - 离线 - 关机失败
// 1 - 在线 - 离线告警
// 2 - 在线 - 开机失败
// onlineCheck95 95在线检查
func onlineCheck95(robotID string, dstStatus int) bool {
	var status int // 0-离线 1-在线
	res := dao.Terminal{}.OnlineStatus(context.Background(), dal.Q, robotID, "95")
	if res {
		status = 1
	}
	if dstStatus == 2 {
		dstStatus = 1
	}
	return dstStatus == status
}

// onlineCheck97 97在线检查
func onlineCheck97(robotID string, dstStatus int) bool {
	var status int // 0-离线 1-在线
	res := dao.Terminal{}.OnlineStatus(context.Background(), dal.Q, robotID, "97")
	if res {
		status = 1
	}
	if dstStatus == 2 {
		dstStatus = 1
	}
	return dstStatus == status
}

// onlineCheck98 98在线检查
func onlineCheck98(robotID string, dstStatus int) bool {
	var status int // 0-离线 1-在线
	res := dao.Terminal{}.OnlineStatus(context.Background(), dal.Q, robotID, "98")
	if res {
		status = 1
	}
	if dstStatus == 2 {
		dstStatus = 1
	}
	return dstStatus == status
}

// lowBatteryCheck 低电量检查
func lowBatteryCheck(robotID string, dstStatus int) bool {
	var status int
	lowBatteryTh := common.GetLowBatteryTh(robotID)
	battery := common.GetBattery(robotID)
	if battery < lowBatteryTh {
		status = 1
	} else if battery > lowBatteryTh {
		status = 0
	}
	return dstStatus == status
}
